/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 * 
 * Copyright (c) 2008-2009, The KiWi Project (http://www.kiwi-project.eu)
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * - Redistributions of source code must retain the above copyright notice, 
 *   this list of conditions and the following disclaimer.
 * - Redistributions in binary form must reproduce the above copyright notice, 
 *   this list of conditions and the following disclaimer in the documentation 
 *   and/or other materials provided with the distribution.
 * - Neither the name of the KiWi Project nor the names of its contributors 
 *   may be used to endorse or promote products derived from this software 
 *   without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE 
 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE 
 * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 * POSSIBILITY OF SUCH DAMAGE.
 * 
 * Contributor(s):
 * 
 * 
 */
package kiwi.core.services.triplestore;

import kiwi.core.api.cache.QueryCachingService;
import kiwi.core.api.config.ConfigurationService;
import kiwi.core.api.persistence.PersistenceService;
import kiwi.core.api.transaction.TransactionData;
import kiwi.core.api.transaction.TransactionService;
import kiwi.core.api.triplestore.KnowledgeSpaceService;
import kiwi.core.api.triplestore.ResourceService;
import kiwi.core.api.triplestore.TripleStore;
import kiwi.core.events.NamespaceEvent;
import kiwi.core.events.TripleEvent;
import kiwi.core.exception.transaction.TransactionNotActiveException;
import kiwi.core.model.rdf.*;
import kiwi.core.model.user.KiWiUser;
import kiwi.core.qualifiers.cache.KiWiCache;
import kiwi.core.qualifiers.event.Created;
import kiwi.core.qualifiers.event.Removed;
import kiwi.core.qualifiers.user.CurrentUser;
import net.sf.ehcache.Cache;
import net.sf.ehcache.Element;
import org.slf4j.Logger;

import javax.annotation.PostConstruct;
import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.event.Event;
import javax.inject.Inject;
import javax.inject.Named;
import javax.persistence.*;
import java.io.Serializable;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;

// ESCA-JAVA0136:
/**
 * The KiWi TripleStore is an component for managing the RDF metadata associated with
 * KiWi Entities. It provides an abstraction layer for storing and retrieving RDF triples,
 * and provides persist and refesh methods that extract / set Java properties of
 * KiWi Entities to / from RDF.
 * <p>
 * The TripleStore stores triples primarily in the relational database inside the table
 * TRIPLES, together with additional triple maintenance data that is used inside the 
 * KiWi system for working with the triple. Secondarily, the triple data is currently
 * stored in the Sesame triple store for efficient SPARQL querying.
 * <p>
 * In the future, we aim to rewrite SPARQL queries to OQL to improve querying performance.
 * Reasoning will be realised using the rule-based reasoner developed in WP2. Reasoning
 * can be efficiently implemented on top of the database because of reason maintenance.
 * <p>
 * Additionally, future versions of the triple store will support multiple knowledge spaces
 * that the user can enable/disable and select for working.
 * <p>
 * Events:
 * <ul>
 *   <li>tripleStoreUpdated - when there are new triples in the triple store or when triples 
 *       have been deleted</li>
 * </ul>
 * 
 * @see KiWiTriple
 * @author Sebastian Schaffert
 *
 */
@Named("kiwi.core.tripleStore")
@ApplicationScoped
public class TripleStoreImpl implements TripleStore, Serializable {
	
	// cache name for namespace prefixes for passed resource uris
	public static String CACHE_NS_BY_RESOURCE = "NS-RESOURCE";
	
	// cache name for namespaces by prefix
	public static String CACHE_NS_BY_PREFIX   = "NS-PREFIX";
	

	/**
	 * Get the seam logger for issuing logging statements.
	 */
    @Inject
	private Logger log;

    


    /**
     * Inject the persistence service so we can get entity managers manually for querying
     */
    @Inject
    private PersistenceService persistenceService;

 	/**
 	 * The transaction service is used for managing transaction-related information.
 	 */
 	@Inject
 	private TransactionService transactionService;
  	
 	/**
 	 * The current user. Used for storing user information in revisions.
 	 */
 	@Inject @CurrentUser
 	private KiWiUser currentUser;
 	
 	
 	/**
 	 * The cache provider defined in Seam. Used extensively for caching triples and nodes in order to 
 	 * avoid unnecessary database access.
 	 */
 	@Inject @KiWiCache("triple-cache")
 	private Cache tripleCache;
 	

    @Inject @KiWiCache("namespace-prefix-cache")
    private Cache namespacePrefixCache;

    @Inject @KiWiCache("namespace-uri-cache")
    private Cache namespaceUriCache;


    @Inject
    private QueryCachingService queryCachingService;

    /**
     * Event queue for created triples
     */
    @Inject @Created
    private Event<TripleEvent> createdTripleEvent;

    /**
     * Event queue for removed triples
     */
    @Inject @Removed
    private Event<TripleEvent> removedTripleEvent;


    /**
     * Event queue for created namespaces
     */
    @Inject @Created
    private Event<NamespaceEvent> createdNamespaceEvent;

    /**
     * Event queue for removed namespaces
     */
    @Inject @Removed
    private Event<NamespaceEvent> removedNamespaceEvent;


    /**
     * The KiWi Configuration Service for looking up configuration details
     */
    @Inject
    private ConfigurationService configurationService;



    @Inject
    private KnowledgeSpaceService knowledgeSpaceService;


    @Inject
    private ResourceService resourceService;


 	/**
 	 * When calling createTriple, first check whether the triple already exists. Since this can have
 	 * a considerable performance penalty, it is possible to switch this to false for large imports
 	 * where the one can be sure that the triple does not yet exist.
 	 */
 	private Boolean noTripleExistsCheck;



    /**
     * Create a new KiWi triple store.
     */
    public TripleStoreImpl() {
		noTripleExistsCheck = false;
    }



    @PostConstruct
    public void initialize() {
        log.info("KiWi Triple Store starting up ...");

    }
    
     /**
     * List all triples contained in this Knowledge Space.
     * @return
     */
    public Set<KiWiTriple> listTriples(boolean inferred) {

        KiWiUriResource context = null;

        if(inferred) {
           context = (KiWiUriResource) knowledgeSpaceService.getInferredKnowledgeSpace();
        }

        return listTriples(null,null,null,context);
    }

    /**
     * Check whether the triple formed by the parameters exists.
     * @param subject
     * @param property
     * @param object
     * @return
     */
    public boolean hasTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object) {
    	return hasTriple(subject,property,object,null);
    }

    public boolean hasTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context) {
    	String key = TripleStoreUtil.createCacheKey(subject, property, object, context);

    	KiWiTriple result = getCachedTriple(key);
    	if(result == null) {
            return (listTriples(subject,property,object,context).size() > 0);
    	} else {
    		return true;
    	}
    }
    

    private void createTripleInternal(KiWiTriple t) {
        // add the triple to the triple cache
        putCachedTriple(TripleStoreUtil.createCacheKey(t),t);

        if(!transactionService.getTransaction().isActive()) {
            log.error("cannot create triple {}, the transaction {} is not active",t,transactionService.getTransaction());
            throw new TransactionNotActiveException("cannot create triple "+t+", the transaction "+transactionService.getTransaction()+" is not active");
        }


        // add the triple to the transaction data (added triples)
        transactionService.getTransaction().getData().getAddedTriples().add(t);

        if(t.isNew()) {
            // fire the created triple event
            createdTripleEvent.fire(new TripleEvent(t));
        }

    }


    /**
     * Create or retrieve a KiWi triple in this knowledge space using the provided subject, predicate, and 
     * object. 
     * 
     * If a triple with the same subject/property/object already exists in the knowledge base,
     * it is returned.
     * 
     * @param subject
     * @param property
     * @param object
     * @return the newly constructed triple
     */
    public synchronized KiWiTriple createTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource space) {


        if(space == null) {
            if(subject.getContext() != null) {
                space = subject.getContext();
            } else {
                space = knowledgeSpaceService.getDefaultKnowledgeSpace();
            }
        }

    	
    	log.debug("creating triple: subject={}, property={}, object={}, space={} ",new Object[] {subject,property,object,space});
    	
		String key =  TripleStoreUtil.createCacheKey(subject, property, object, space);
		
		KiWiTriple result = getCachedTriple(key);
		if(result == null || result.isDeleted()) {
			if(isCheckTripleExists() && subject.getId() != null && property.getId() != null && object.getId() != null && space.getId() != null) {
    			// if check is turned on, do a more expensive check whether a triple already exists ...
 
    			try {
    				result = getTripleBySPOC(subject, property, object, space);
    				result.setNew(false);
    			} catch(NoResultException ex) {
    				result = new KiWiTriple(subject, property, object, space);
    				result.setAuthor(currentUser);
    				result.setNew(true);
    			} catch(NonUniqueResultException ex) {
    				log.error("non-unique result");
    			}
           		putCachedTriple(key, result);
			} else {
				// if check is turned off, insert the triple without checking and return it

				result = new KiWiTriple(subject, property, object, space);
				result.setAuthor(currentUser);
				result.setNew(true);
			}
			// ensure that triple is added to "addedTriples" in transaction
			log.debug("storing new triple {} ", result);

		} else {
            log.debug("triple already cached");
            result.setNew(false);
        }
		
		if(result == null) {
			log.error("result of createTriple is null - this is a bug in Hibernate and should never happen!");			
		}

        createTripleInternal(result);

		return result;
    }
 


    


   /**
     * Remove a triple from the repository
     * @param triple
     */
    public synchronized void removeTriple(KiWiTriple triple) {

 		// remove from cache
 		removeCachedTriple(TripleStoreUtil.createCacheKey(triple));
     	
    	// add triple to current transaction data
        transactionService.getTransaction().getData().getRemovedTriples().add(triple);

        // raise triple removed event
        removedTripleEvent.fire(new TripleEvent(triple));
    }


    /**
     * Remove a triple from the repository with the given subject, property, and object
     * @param subject
     * @param property
     * @param object
     */
    public void removeTriple(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context)  {
    	//
		String key =  TripleStoreUtil.createCacheKey(subject, property, object, context);

		KiWiTriple result = getCachedTriple(key);
		if(result == null) {
			try {
				removeTriple(getTripleBySPOC(subject, property, object, context));
			} catch(NoResultException ex) {}
		} else
			removeTriple(result);
    }


    /**
     * Remove all triples matching the pattern passed in the parameters. Arguments that are passed as "null" are
     * considered wildcards like in listTriples, i.e. all values match. Inferred triples are not included.
     *
     * @param subject  the subject of the triples to list or null for wildcard
     * @param property the property of the triples to list or null for wildcard
     * @param object   the object of the triples to list or null for wildcard
     * @param context  the context/knowledge space of the triples to list or null for all spaces
     */
    @Override
    public void removeTriples(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context) {
        for(KiWiTriple triple : listTriples(subject,property,object,context,false)) {
            removeTriple(triple);
        }
    }



    /**
     * List all triples matching the subject, property, object, and context given as argument. Parameters that are null
     * are treated as wildcards, i.e. all values match. Inferred values are included.
     *
     * @param subject  the subject of the triples to list or null for wildcard
     * @param property the property of the triples to list or null for wildcard
     * @param object   the object of the triples to list or null for wildcard
     * @param context  the context/knowledge space of the triples to list or null for all spaces
     * @return a List of triples matching the search criteria
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context) {
        return listTriples(subject,property,object,context,true);
    }


    /**
     * List all triples matching the subject, property, object, and context given as argument. Parameters that are null
     * are treated as wildcards, i.e. all values match. If the parameter inferred is true, inferred triples are included,
     * otherwise inferred triples are not included.
     *
     * @param subject  the subject of the triples to list or null for wildcard
     * @param property the property of the triples to list or null for wildcard
     * @param object   the object of the triples to list or null for wildcard
     * @param context  the context/knowledge space of the triples to list or null for all spaces
     * @param inferred if true, inferred triples are included in the result; if false not
     * @return a List of triples matching the search criteria
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context, boolean inferred) {
        return listTriples(subject,property,object,context,inferred,0,0);
    }

    /**
     * List all triples matching the subject, property, object, and all context given as argument. Parameters that are null
     * are treated as wildcards, i.e. all values match. If the parameter inferred is true, inferred triples are included,
     * otherwise inferred triples are not included.
     *
     * @param subject  the subject of the triples to list or null for wildcard
     * @param property the property of the triples to list or null for wildcard
     * @param object   the object of the triples to list or null for wildcard
     * @param contextSet  the context/knowledge spaces of the triples to list or null for all spaces
     * @param inferred if true, inferred triples are included in the result; if false not
     * @return a List of triples matching the search criteria
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject, KiWiUriResource property, KiWiNode object, Set<KiWiUriResource> contextSet, boolean inferred, int offset, int limit) {

        if(contextSet == null) {

            return listTriples(subject,property,object,(KiWiUriResource) null,inferred,offset,limit);
        }

        Iterator<KiWiUriResource> contextIter = contextSet.iterator();
        Set<KiWiTriple> triple = new HashSet();

        while(contextIter.hasNext()) {

            triple.addAll(listTriples(subject,property,object,contextIter.next(),inferred,offset,limit));
        }

        return triple;
    }

    /**
     * List all triples matching the subject, property, object, and context given as argument. Parameters that are null
     * are treated as wildcards, i.e. all values match. If the parameter inferred is true, inferred triples are included,
     * otherwise inferred triples are not included.
     *
     * @param subject  the subject of the triples to list or null for wildcard
     * @param property the property of the triples to list or null for wildcard
     * @param object   the object of the triples to list or null for wildcard
     * @param context  the context/knowledge space of the triples to list or null for all spaces
     * @param inferred if true, inferred triples are included in the result; if false not
     * @return a List of triples matching the search criteria
     */
    @Override
    public Set<KiWiTriple> listTriples(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context, boolean inferred, int offset, int limit) {

        if(offset > 0) {
            log.warn("setting offset currently has no effect, there is an internal modelling problem");
        }


        Set<KiWiTriple> result = queryCachingService.listTriples(subject,property,object,context,inferred);


        if(result == null) {

            result = new HashSet<KiWiTriple>();

            // if all of the given arguments are in the database (have an ID), issue a database query
            if( (subject  == null || subject.getId() != null) &&
                    (property == null || property.getId() != null) &&
                    (object   == null || object.getId() != null) &&
                    (context  == null || context.getId() != null)     ) {

                long start = System.currentTimeMillis();

                // build the query name according to the parameters that have been set
                String queryName = "tripleStore.listTriplesBy";
                if(subject != null) {
                    queryName += "S";
                }
                if(property != null) {
                    queryName += "P";
                }
                if(object != null) {
                    queryName += "O";
                }
                if(context != null) {
                    queryName += "C";
                }
//                if(inferred) {
//                    queryName +=".Inferred";
//                }


                log.debug("listTriples: executing named database query '{}'",queryName);


                EntityManager entityManager = persistenceService.getEntityManager();
                EntityTransaction tx = entityManager.getTransaction();

                tx.begin();
                // create the query and set the parameters
                Query query = entityManager.createNamedQuery(queryName);
                query.setHint("org.hibernate.cacheable", true);
                if(subject != null) {
                    query.setParameter("subject",subject);
                }
                if(property != null) {
                    query.setParameter("property",property);
                }
                if(object != null) {
                    query.setParameter("object",object);
                }
                if(context != null) {
                    query.setParameter("context",context);
                }
                result.addAll(query.getResultList());

                tx.commit();
                persistenceService.closeEntityManager(entityManager);

                if(log.isDebugEnabled())
                    log.debug("listTriples: executed named database query '{}', {} results in {} ms",new Object[] {queryName,result.size(),System.currentTimeMillis()-start});
            }


            // if the transaction is active, we also need to query the data contained in it; note that this is not
            // necessary for transactions that are committed already (e.g. in the @AfterCommit event) because then
            // the data will already be contained in the database query
            if(transactionService.getTransaction().isActive() &&
                    ( transactionService.getTransaction().getData().getAddedTriples().size() > 0 ||
                            transactionService.getTransaction().getData().getRemovedTriples().size() > 0  ) ) {

                long start = System.currentTimeMillis();

                // now look for all matching triples in the current transaction
                TransactionData data = transactionService.getTransaction().getData();

                // perform cleanup first (cf. TransactionServiceImpl)
                HashSet<KiWiTriple> addedTriples = new HashSet<KiWiTriple>(data.getAddedTriples());
                HashSet<KiWiTriple> removedTriples = new HashSet<KiWiTriple>(data.getRemovedTriples());
                if(addedTriples.size() > 0 && removedTriples.size() > 0) {
                    for(KiWiTriple added : new HashSet<KiWiTriple>(addedTriples)) {
                        if(removedTriples.remove(added)) {
                            addedTriples.remove(added);
                        }
                    }
                }


                // update the result list with results from transaction

                // remove all triples from result that have been removed in transaction; we simply iterate over all
                // removed nodes and remove them from the result set
                if(removedTriples.size() > 0) {
                    for(KiWiTriple triple : removedTriples) {
                        result.remove(triple);
                    }
                }

                // filter the added nodes based on the search criteria and add them to the result
                if(addedTriples.size() > 0) {
                    if(subject != null) {
                        for(Iterator<KiWiTriple> it = addedTriples.iterator(); it.hasNext(); ) {
                            KiWiTriple triple = it.next();
                            if(!triple.getSubject().equals(subject)) {
                                it.remove();
                            }
                        }
                    }
                    if(property != null) {
                        for(Iterator<KiWiTriple> it = addedTriples.iterator(); it.hasNext(); ) {
                            KiWiTriple triple = it.next();
                            if(!triple.getProperty().equals(property)) {
                                it.remove();
                            }
                        }
                    }
                    if(object != null) {
                        for(Iterator<KiWiTriple> it = addedTriples.iterator(); it.hasNext(); ) {
                            KiWiTriple triple = it.next();
                            if(!triple.getObject().equals(object)) {
                                it.remove();
                            }
                        }
                    }
                    if(context != null) {
                        for(Iterator<KiWiTriple> it = addedTriples.iterator(); it.hasNext(); ) {
                            KiWiTriple triple = it.next();
                            if(!triple.getContext().equals(context)) {
                                it.remove();
                            }
                        }
                    }
                    result.addAll(addedTriples);
                    if(log.isDebugEnabled())
                        log.debug("listTriples: executed query on transaction data with {} results in {} ms",addedTriples.size(),System.currentTimeMillis()-start);
                }
            }

            queryCachingService.cacheTriples(subject,property,object,context,inferred,result);
        }

        if(limit > 0) {
            Set<KiWiTriple> limitedResult = new HashSet<KiWiTriple>();

            for(KiWiTriple triple : result) {
                limitedResult.add(triple);
                if(limitedResult.size() >= limit) {
                    break;
                }
            }

            return limitedResult;
        } else {
            return result;
        }

    }

    private KiWiTriple getTripleBySPO(KiWiResource subject, KiWiUriResource property, KiWiNode object) {
        Set<KiWiTriple> triples = listTriples(subject,property,object,null,true);

        if(triples.size() > 0) {
            return triples.iterator().next();
        } else {
            throw new NoResultException("no result");
        }
     }


    private KiWiTriple getTripleBySPOC(KiWiResource subject, KiWiUriResource property, KiWiNode object, KiWiUriResource context) {
        Set<KiWiTriple> triples = listTriples(subject,property,object,context,true);

        if(triples.size() > 0) {
            return triples.iterator().next();
        } else {
            throw new NoResultException("no result");
        }
    }

    




	/**
	 * Clear all caches that are currently active; used e.g. in case of a rollback to avoid 
	 * detached triples that are not really persisted.
	 */
	public void clearCaches() {
        tripleCache.removeAll();
 	}
	
	public synchronized void clear() {
		// the following procedure is very inefficient; for performance reasons, we should rather 
		// do the following on transaction commit
		// (1) select all triple ids of non-deleted triples and store them in the metadataupdate
		//     removed relations (maybe there is a HQL query that can be used like a nested SQL?)
		// (2) run a update query "update KiWiTriple t set t.deleted = true where t.deleted = false"
		// (3) update the cacheProvider by removing all nodes in /triples and /namespaces
		
        Iterable<KiWiTriple> triples = listTriples(true);
        Iterable<KiWiNamespace> namespaces = listNamespaces();
        for(KiWiTriple t : triples) {
         	removeTriple(t);
        	
 			removeCachedTriple(TripleStoreUtil.createCacheKey(t));
        }
        for(KiWiNamespace n : namespaces) {
        	removeNamespace(n);
        }
    }
    
    public synchronized String getNamespaceUri(String prefix) {
        return getNamespaceByPrefix(prefix).getUri();
    }
        
     
    public synchronized KiWiNamespace getNamespaceByPrefix(String prefix) {
    	KiWiNamespace ns;
        if(namespacePrefixCache.get(prefix) != null) {
            ns = (KiWiNamespace) namespacePrefixCache.get(prefix).getObjectValue();
        } else {
            EntityManager entityManager = persistenceService.getEntityManager();
            EntityTransaction tx = entityManager.getTransaction();

            tx.begin();
	    	Query q = entityManager.createNamedQuery("tripleStore.namespaceByPrefix");
	    	q.setParameter("prefix", prefix);
			q.setHint("org.hibernate.cacheable", true);
			q.setMaxResults(1);
	    	
	    	try {
	    		ns = (KiWiNamespace) q.getSingleResult();

                putCachedNamespace(ns);
	    	} catch(NoResultException ex) {
	    		log.debug("namespace with prefix '{}' could not be found.",prefix);
	    		ns = null;
	    	}
            tx.commit();
            persistenceService.closeEntityManager(entityManager);
    	}

        if(transactionService.getTransaction().isActive() &&
           ( transactionService.getTransaction().getData().getAddedNS().size() > 0 ||
             transactionService.getTransaction().getData().getRemovedNS().size() > 0  ) ) {

            long start = System.currentTimeMillis();

            // now look for all matching namespaces in the current transaction
            TransactionData data = transactionService.getTransaction().getData();

            // perform cleanup first (cf. TransactionServiceImpl)
            HashSet<KiWiNamespace> addedNS = new HashSet<KiWiNamespace>(data.getAddedNS());
            HashSet<KiWiNamespace> removedNS = new HashSet<KiWiNamespace>(data.getRemovedNS());
            if(addedNS.size() > 0 && removedNS.size() > 0) {
                for(KiWiNamespace added : data.getAddedNS()) {
                    if(removedNS.remove(added)) {
                        addedNS.remove(added);
                    }
                }
            }

            if(removedNS.contains(ns)) {
                ns = null;
            }

            for(KiWiNamespace added : addedNS) {
                if(added.getPrefix().equals(prefix)) {
                    ns = added;
                    break;
                }
            }
        }

        return ns;
    }
  
    /**
     * Return the KiWi namespace that matches for the uri passed as argument. It is sufficient 
     * that the uri of the namespace is a prefix for the given uri for this method to
     * return the matching namespace.
     * 
     * @param uri the uri of which the namespace is a prefix
     * @return the namespace matching the passed uri
     */
    public KiWiNamespace getNamespaceByUri(String uri) {
        KiWiNamespace ns;

        if(namespaceUriCache.get(uri) != null) {
            ns = (KiWiNamespace) namespaceUriCache.get(uri).getObjectValue();
        } else {
            EntityManager entityManager = persistenceService.getEntityManager();
            EntityTransaction tx = entityManager.getTransaction();

            tx.begin();

            Query q = entityManager.createNamedQuery("tripleStore.namespaceByUriPrefix");
            q.setParameter("uri", uri);
            q.setHint("org.hibernate.cacheable", true);

            q.setMaxResults(1);

            try {
                ns = (KiWiNamespace) q.getSingleResult();

                putCachedNamespace(ns);
            } catch(NoResultException ex) {
                log.debug("namespace for uri '{}' could not be found.",uri);
                ns = null;
            }

            tx.commit();
            persistenceService.closeEntityManager(entityManager);
        }


        if(transactionService.getTransaction().isActive() &&
           ( transactionService.getTransaction().getData().getAddedNS().size() > 0 ||
             transactionService.getTransaction().getData().getRemovedNS().size() > 0  ) ) {

            long start = System.currentTimeMillis();

            // now look for all matching namespaces in the current transaction
            TransactionData data = transactionService.getTransaction().getData();

            // perform cleanup first (cf. TransactionServiceImpl)
            HashSet<KiWiNamespace> addedNS = new HashSet<KiWiNamespace>(data.getAddedNS());
            HashSet<KiWiNamespace> removedNS = new HashSet<KiWiNamespace>(data.getRemovedNS());
            if(addedNS.size() > 0 && removedNS.size() > 0) {
                for(KiWiNamespace added : data.getAddedNS()) {
                    if(removedNS.remove(added)) {
                        addedNS.remove(added);
                    }
                }
            }

            if(removedNS.contains(ns)) {
                ns = null;
            }

            for(KiWiNamespace added : addedNS) {
                if(added.getUri().equals(uri)) {
                    ns = added;
                    break;
                }
            }
        }


        return ns;
    }
    
    private synchronized void setNamespace(KiWiNamespace ns) {
        putCachedNamespace(ns);

        transactionService.getTransaction().getData().getAddedNS().add(ns);
    }
  
    public void setNamespace(String prefix, String uri)  {
    	KiWiNamespace ns = getNamespaceByUri(uri);
    	if(ns != null) {
            // remove existing namespace definition for the URI
            removeNamespace(ns);
            log.debug("overwriting namespace prefix {} for URI {} with {}", new Object[]{ns.getPrefix(),ns.getUri(),prefix});

    	} else {
            log.debug("setting new namespace prefix {} for URI {}",prefix,uri);
        }


        // check whether prefix already exists; if yes, attach a sequential number to it
        KiWiNamespace _ns = getNamespaceByPrefix(prefix);
        String myprefix = prefix;
        int count = 1;
        while(_ns != null) {
            myprefix = prefix + count++;
            _ns = getNamespaceByPrefix(myprefix);
        }

        ns = new KiWiNamespace(myprefix,uri);
    	setNamespace(ns);
    }
        
    public void removeNamespace(String prefix) {
    	KiWiNamespace ns = null;
    	if((ns = getNamespaceByPrefix(prefix)) != null) {
    		removeNamespace(ns);
    	} else {
    		log.debug("KiwiNamespace with prefix {} is already deleted", prefix);
    	}
    }
    
    public synchronized void removeNamespace(KiWiNamespace ns) {
    	log.debug("removing namespace with id {}, prefix '{}' and uri {}",new Object[] {ns.getId(),ns.getPrefix(),ns.getUri()});
 	    removeCachedNamespace(ns);


    	/* (changed by steffi) To be able to attach every update action to a ContentItem, 
     	we need to forbear to keep track of changes in namespaces (they can be regenerated, anyways) */
        transactionService.getTransaction().getData().getRemovedNS().add(ns);

    	
    	// fire an event to indicate that the namespace has been removed
        removedNamespaceEvent.fire(new NamespaceEvent(ns));
    }
    
    /**
     * Return the namespace prefix of the passed uri. In order to match, it is sufficient that the
     * uri stored in the database is a prefix of the uri passed as argument.
     * 
     * @param uri the uri to return a namespace for
     * @return the namespace prefix (e.g. "rdf") of the uri
     */
    @Override
    public String getNamespacePrefix(String uri) {
        return getNamespaceByUri(uri).getPrefix();
    }
    
    
    /**
     * List all namespaces defined in the triple store.
     * 
     * @return a list of all namespaces defined in the triple store.
     */
    @SuppressWarnings("unchecked")
	public Iterable<KiWiNamespace> listNamespaces() {
        EntityManager entityManager = persistenceService.getEntityManager();
        EntityTransaction tx = entityManager.getTransaction();
        try {

            tx.begin();

            Query q = entityManager.createNamedQuery("tripleStore.listNamespaces");
            q.setHint("org.hibernate.cacheable", true);
            return (Iterable<KiWiNamespace>) q.getResultList();
        } finally {
            tx.commit();
            persistenceService.closeEntityManager(entityManager);
        }
    }
    


    private String createCacheKey(KiWiResource subject, String property_uri, KiWiNode object, KiWiResource context) {
    	if(context != null) {
    		return "{"+subject.toString()+" "+property_uri+" "+object.toString()+"}@"+context.toString();   
    	} else {
    		return "{"+subject.toString()+" "+property_uri+" "+object.toString()+"}@GLOBAL";
    	}
    }
    
    
	/**
	 * @return the !noTripleExistsCheck
	 */
	public boolean isCheckTripleExists() {
		if(noTripleExistsCheck == null) {
			noTripleExistsCheck = false;
		}
		return !noTripleExistsCheck;
	}

	/**
	 * @param !noTripleExistsCheck the !noTripleExistsCheck to set
	 */
	public void setCheckTripleExists(boolean checkTripleExists) {
		this.noTripleExistsCheck = !checkTripleExists;
	}
    
    
	private KiWiTriple getCachedTriple(String key) {
        if(tripleCache.get(key) != null) {
            return (KiWiTriple)tripleCache.get(key).getObjectValue();
        } else {
            return null;
        }

 	}
	
	private void putCachedTriple(String key, KiWiTriple triple) {
        tripleCache.put(new Element(key,triple));
 	}
	
	private void removeCachedTriple(String key) {
        tripleCache.remove(key);
	}


    private void putCachedNamespace(KiWiNamespace ns) {
        namespacePrefixCache.put(new Element(ns.getPrefix(),ns));
        namespaceUriCache.put(new Element(ns.getUri(),ns));
    }


    private void removeCachedNamespace(KiWiNamespace ns) {
        namespacePrefixCache.remove(ns.getPrefix());
        namespaceUriCache.remove(ns.getUri());
    }



	/**
     * Add or update a triple to/in the triple store. This method only adds the triple to the
     * current transaction data; the triple will be persisted to the database and repository
     * on transaction commit. 
     * 
     * @param triple
     */
	public void storeTriple(KiWiTriple triple) {
		log.debug("storing triple {} ", triple);
        transactionService.getTransaction().getData().getAddedTriples().add(triple);
    }


    /**
     * Return the number of non-deleted triples currently contained in the triple store
     *
     * @return
     */
    @Override
    public long size() {
        EntityManager entityManager = persistenceService.getEntityManager();
        EntityTransaction tx = entityManager.getTransaction();
        try {

            tx.begin();

            Query q = entityManager.createNamedQuery("contentItemService.count");
            q.setHint("org.hibernate.cacheable", true);
            q.setMaxResults(1);

            return (Long) q.getSingleResult();
        } finally {
            tx.commit();
            persistenceService.closeEntityManager(entityManager);
        }
     }
}
